use crate::{utils::get_context_key, AppError, AppResult, AppState};
use ane_cms::dao::{ArticleListVo, ArticleVo};
use axum::{
    extract::{MatchedPath, NestedPath, Path, Query, State},
    http::{uri::Parts, Uri},
    response::{IntoResponse, Response},
    Extension, Json, Router,
};
use axum::{handler::Handler, routing::get};
use common::page::PageParams;
use core::sync;
use fred::prelude::SetsInterface;
use futures::future::{BoxFuture, LocalBoxFuture};
use once_cell::sync::Lazy;
use regex::Regex;
use sea_orm::{query, sea_query::Nullable, DbConn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, env::args, fmt::format, future::Future, pin::Pin, sync::Arc};

type JsonValue = serde_json::Value;
type JsonMap = serde_json::Map<String, JsonValue>;

/// 函数参数
#[derive(Debug)]
struct ArgValue(JsonValue);

impl ArgValue {
    pub fn try_to_i32(&self) -> Option<i32> {
        if self.0.is_number() {
            self.0
                .as_number()
                .and_then(|f| f.as_i64().map(|f| f as i32))
        } else if self.0.is_string() {
            self.0.as_str().and_then(|f| f.parse::<i32>().ok())
        } else {
            None
        }
    }

    pub fn to_i32(&self) -> AppResult<i32> {
        self.try_to_i32().ok_or_else(|| {
            let error = format!("error type: '{}' is not 'i32' type", self.0);
            tracing::debug!(error);
            AppError::BadParam(error)
        })
    }

    pub fn try_to_u64(&self) -> Option<u64> {
        if self.0.is_number() {
            self.0.as_number().and_then(|f| f.as_u64())
        } else if self.0.is_string() {
            self.0.as_str().and_then(|f| f.parse::<u64>().ok())
        } else {
            None
        }
    }

    pub fn to_u64(&self) -> AppResult<u64> {
        self.try_to_u64().ok_or_else(|| {
            let error = format!("error type: '{}' is not 'u64' type", self.0);
            tracing::debug!(error);
            AppError::BadParam(error)
        })
    }

    pub fn try_to_string(&self) -> Option<String> {
        if self.0.is_number() {
            self.0.as_number().map(|f| f.to_string())
        } else if self.0.is_string() {
            self.0.as_str().map(|f| f.to_owned())
        } else {
            None
        }
    }
}

impl TryInto<i32> for &ArgValue {
    type Error = AppError;

    fn try_into(self) -> Result<i32, Self::Error> {
        self.to_i32()
    }
}

impl TryInto<Option<i32>> for &ArgValue {
    type Error = AppError;

    fn try_into(self) -> Result<Option<i32>, Self::Error> {
        Ok(self.try_to_i32())
    }
}

// trait Function: Send + Sized {
//     type Future: Future<Output = Result<JsonValue, AppError>> + Send;
//
//     /// Call the handler with the given request.
//     fn call1(self, db: Pin<&DbConn>) -> Self::Future;
// }
//
// trait Handler1 {
//     type Input;
// }
//
// impl<'a, F, Fut, S> Function for F
// where
//     F: Fn(&'a DbConn) -> Fut + Sync + Send + 'static + Handler1<Input = (&'a DbConn)>,
//     Fut: Future<Output = Result<S, sea_orm::DbErr>> + Send,
//     S: Serialize,
// {
//     type Future = Pin<Box<dyn Future<Output = Result<JsonValue, AppError>> + Send>>;
//     fn call1(self, db: Pin<&DbConn>) -> Self::Future {
//         Box::pin(async move {
//             self(&db)
//                 .await
//                 .map(|f| serde_json::to_value(f).unwrap())
//                 .map_err(Into::into)
//         })
//     }
// }

/// Cms提供的函数
#[derive(Debug, Clone, Deserialize)]
enum CmsFunc {
    /// 列出分类 [ane_cms::dao::Cms::fetch_category]
    ListCategory,
    /// 获取文章  [ane_cms::dao::Cms::fetch_article]
    FetchArticle(String),
    /// 列出文章  [ane_cms::dao::Cms::list_article]
    ListArticle(String, String, String, String),
}

impl CmsFunc {
    pub async fn exec<F>(&self, db: &DbConn, map_key: F) -> Result<JsonValue, AppError>
    where
        F: Fn(&str) -> ArgValue,
    {
        // return ane_cms::dao::Cms::fetch_category.call1(db).await;
        match self {
            CmsFunc::ListCategory => ane_cms::dao::Cms::fetch_category(db, Some(1))
                .await
                .map(|f| serde_json::to_value(f).unwrap())
                .map_err(Into::into),
            CmsFunc::FetchArticle(id) => {
                ane_cms::dao::Cms::fetch_article::<ArticleVo>(db, (&map_key(&id)).try_into()?)
                    .await
                    .map(|f| serde_json::to_value(f).unwrap())
                    .map_err(Into::into)
            }
            CmsFunc::ListArticle(page, page_size, category_id, key) => {
                ane_cms::dao::Cms::list_article::<ArticleListVo>(
                    db,
                    map_key(&page).to_u64()?,
                    map_key(&page_size).to_u64()?,
                    (&map_key(&category_id)).try_into()?,
                    map_key(&key).try_to_string(),
                )
                .await
                .map(|f| serde_json::to_value(f).unwrap())
                .map_err(Into::into)
            }
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct CmsRouter {
    url: String,
    template: String,
    #[serde(default)]
    context: JsonMap,
    #[serde(default)]
    datas: HashMap<String, CmsFunc>,
}

#[derive(Debug, Deserialize)]
pub struct CmsConfig {
    #[serde(default)]
    context: JsonMap,
    routers: HashMap<String, CmsRouter>,
}

fn load_config() -> CmsConfig {
    let text = std::fs::read_to_string("config/cms.yml").expect("miss file `config/cms.yml`");
    serde_yaml::from_str::<CmsConfig>(&text).expect("`config/cms.yml` error yaml format")
}

pub fn get_route() -> Router<AppState> {
    println!("Start loading Cms configuration");
    let mut router = Router::new();
    let cfgs = load_config();
    for (_, cms_router) in &cfgs.routers {
        let handle = render.layer(Extension(Arc::new(cms_router.clone())));
        router = router.route(&cms_router.url, axum::routing::get(handle));
    }
    println!("Complete loading Cms configuration\n");
    router.layer(Extension(Arc::new(cfgs)))
}

async fn render(
    uri: Uri,
    metched_path: MatchedPath,
    nested_path: Option<NestedPath>,
    method: axum::http::Method,
    Path(paths): Path<HashMap<String, String>>,
    Query(query): Query<HashMap<String, String>>,
    Query(page_params): Query<PageParams>,
    State(state): State<AppState>,
    Extension(cms_config): Extension<Arc<CmsConfig>>,
    Extension(cms_router): Extension<Arc<CmsRouter>>,
) -> AppResult<Response> {
    let mut context = state.tera_context();
    let db = state.db();
    for (key, val) in &cms_config.context {
        context.insert(key, val);
    }
    for (key, val) in &cms_router.context {
        context.insert(key, val);
    }
    context.insert("uri", &uri.path());
    context.insert("path", &paths);
    context.insert("query", &query);
    context.insert("method", &method.to_string());
    context.insert("page", &into_page_map(&page_params));
    for (key, func) in &cms_router.datas {
        let value = func
            .exec(db, |k| {
                if k == "None" {
                    return ArgValue(JsonValue::Null);
                } else {
                    ArgValue(
                        get_context_key(k, &context)
                            .unwrap_or_else(|| JsonValue::String(k.to_owned())),
                    )
                }
            })
            .await?;
        context.insert(key, &value);
    }
    state
        .render_template(&cms_router.template, &context)
        .map(IntoResponse::into_response)
}

fn into_page_map(page_params: &PageParams) -> JsonMap {
    let mut page: JsonMap = JsonMap::new();
    page.insert(
        "size".to_owned(),
        serde_json::Number::from(page_params.page_size()).into(),
    );
    page.insert(
        "page".to_owned(),
        serde_json::Number::from(page_params.page()).into(),
    );
    page.insert(
        "zero_page".to_owned(),
        serde_json::Number::from(page_params.zero_page()).into(),
    );
    page.insert(
        "limit".to_owned(),
        serde_json::Number::from(page_params.limit()).into(),
    );
    page.insert(
        "offset".to_owned(),
        serde_json::Number::from(page_params.offset()).into(),
    );
    page
}

static PATH_PARAMS: Lazy<Regex> = Lazy::new(|| Regex::new(r":[\d\w_]+").unwrap());

pub fn url_for(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
    fn value_to_string(value: &tera::Value) -> String {
        if let Ok(s) = tera::from_value::<String>(value.clone()) {
            s
        } else {
            value.to_string()
        }
    };
    let get_string = |key: &str| match args.get(key) {
        Some(val) => Ok(value_to_string(val)),
        None => Err(tera::Error::from(format!("miss params '{key}'"))),
    };
    let mut keys = vec!["url"];
    let url = get_string("url")?;

    let mut path = "".to_owned();
    let mut pos = 0;
    for i in PATH_PARAMS.find_iter(&url) {
        path += &url[pos..i.start()];
        let key = &i.as_str()[1..];
        let k = get_string(key)?;
        path += &k;
        keys.push(key);
        pos = i.end();
    }
    path += &url[pos..];

    let query = args
        .iter()
        .filter(|(k, v)| !keys.contains(&k.as_str()))
        .map(|(k, v)| format!("{k}={}", value_to_string(v)))
        .collect::<Vec<_>>()
        .join("&");
    if query.len() > 0 {
        path += "?";
        path += &query;
    }
    Ok(tera::Value::String(path))
}

pub fn iter_page(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
    let get_usize = |key: &str| match args.get(key) {
        Some(val) => match tera::from_value::<isize>(val.clone()) {
            Ok(v) => Ok(Some(v)),
            Err(_) => Err(tera::Error::msg(format!(
                "Function `iter_page` received {key}={} but `{key}` can only be a number",
                val
            ))),
        },
        None => Ok(None),
    };

    let pages = get_usize("pages")?.ok_or("Function `iter_page` miss params `pages`")?;
    let page = get_usize("page")?.ok_or("Function `iter_page` miss params `page`")?;
    let left_edge = get_usize("left_edge")?.unwrap_or(1);
    let left_current = get_usize("left_current")?.unwrap_or(1);
    let right_current = get_usize("right_current")?.unwrap_or(2);
    let right_edge = get_usize("right_edge")?.unwrap_or(1);

    let mut datas = vec![];
    let len = left_edge + left_current + 3 + right_current + right_edge;
    let mid_len = left_current + 1 + right_current;
    let right_archor = pages - right_edge + 1;
    if pages <= len {
        for i in 1..pages + 1 {
            datas.push(Some(i));
        }
    } else {
        for i in 1..=left_edge {
            datas.push(Some(i));
        }
        if page - left_current - left_edge - 1 < 0 {
            for i in left_edge + 1..left_edge + mid_len + 2 {
                datas.push(Some(i));
            }
            datas.push(None);
        } else if right_archor - (page + right_current) - 1 < 0 {
            datas.push(None);
            for i in right_archor - mid_len - 1..right_archor {
                datas.push(Some(i));
            }
        } else {
            datas.push(None);
            for i in page - left_current..=page + right_current {
                datas.push(Some(i));
            }
            datas.push(None);
        }
        for i in right_archor..=pages {
            datas.push(Some(i));
        }
    }
    Ok(tera::to_value(datas).unwrap())
}

pub fn markdown(
    value: &tera::Value,
    _: &HashMap<String, tera::Value>,
) -> tera::Result<tera::Value> {
    if let Some(value) = value.as_str() {
        let mut options = pulldown_cmark::Options::empty();
        options.insert(pulldown_cmark::Options::ENABLE_STRIKETHROUGH);
        let parser = pulldown_cmark::Parser::new_ext(value, options);
        let mut html_output = String::new();
        pulldown_cmark::html::push_html(&mut html_output, parser);
        Ok(serde_json::to_value(html_output).unwrap())
    } else {
        Ok(value.to_owned())
    }
}
